"""
这个模块的名字叫views（视图）但实际扮演的是Controller（控制器）的角色
控制器：负责把数据和页面（数据的显示）关联起来
"""
import copy
import logging
import os
import uuid
from datetime import timedelta
from io import BytesIO
from urllib.parse import quote

import jwt
import xlwt
from django.conf import settings
from django.db import DatabaseError
from django.db.models import Q
from django.db.transaction import atomic
from django.http import JsonResponse, HttpResponse
from django.shortcuts import render, redirect
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_exempt
from django_redis import get_redis_connection
from rest_framework.decorators import api_view
from rest_framework.generics import ListAPIView
from rest_framework.response import Response

from polls.models import Subject, Teacher, User
from polls.serializers import SubjectSerializer, TeacherSerializer
from polls.utils import check_username, check_password, gen_sha256_digest, check_tel, send_mobile_code, random_code, \
    upload_stream_to_qiniu, POOL


def show_index(request):
    # 使用原生SQL查询的第一种方式（模型.objects.raw('...')）
    # queryset = Subject.objects.raw(SELECT_HOT_SUBJECTS)
    # 使用原生SQL查询的第二种方式（绕过Django的ORM机制）
    # from django.db.transaction import get_connection
    # conn = get_connection()
    # try:
    #     with conn.cursor() as cursor:
    #         cursor.execute('select no, name, intro from tb_subject where is_hot=1')
    #         print(cursor.fetchall())
    # except MySQLError:
    #     pass
    # finally:
    #     conn.close()
    return redirect('/static/html/subjects.html')


# # FBV - Function-Based View（基于函数的视图）
# # 代码量比较大，但是最灵活，接口返回的数据完全可定制
# @api_view(('GET', ))
# # @cache_page(timeout=86400, cache='default')
# def show_subjects(request):
#     """获取学科数据"""
#     redis_cli = get_redis_connection()
#     # 先尝试从默认的缓存中获取学科数据
#     data = redis_cli.get('vote2003:polls:subjects')
#     if data:
#         # 如果获取到学科数据就进行反序列化操作
#         data = json.loads(data)
#     else:
#         # 如果没有获取到学科数据就查数据库
#         queryset = Subject.objects.all()
#         data = SubjectSerializer(queryset, many=True).data
#         # 将查到的学科数据序列化后放到缓存中
#         redis_cli.set('vote2003:polls:subjects', json.dumps(data), ex=86400)
#     return Response({'code': 20000, 'subjects': data})


# CBV - Class-Based View（基于类的视图）
# 代码量非常少，但是不够灵活，因为父类已经定制好所有的东西
# 如果希望自己定制接口的内容，需要通过重写父类方法来做到
# method_decorator装饰器是一个装饰类的装饰器，它可以把原来的装饰函数的装饰器添加到类的方法上
# 第一个参数decorator就是原来装饰函数的装饰器
# 第二个参数name表示被装饰的方法的名字
@method_decorator(decorator=cache_page(timeout=86400, cache='default'), name='get')
class SubjectView(ListAPIView):
    # 指定如何获取数据
    queryset = Subject.objects.all()
    # 指定如何序列化数据
    serializer_class = SubjectSerializer
    # 指定如何分页
    # pagination_class = CustomizedPagination
    pagination_class = None
    # 指定如何验证用户身份
    # authentication_classes = [LoginRequiredAuthentication, ]

    # def get(self, request, *args, **kwargs):
    #     redis_cli = get_redis_connection()
    #     result = redis_cli.get(...)
    #     if not result:
    #         result = super().get(request, *args, **kwargs)
    #         redis_cli.set(...)
    #     return result


# @method_decorator(decorator=cache_page(timeout=86400, cache='default'), name='list')
# @method_decorator(decorator=cache_page(timeout=86400, cache='default'), name='retrieve')
# class SubjectViewSet(ModelViewSet):
#     queryset = Subject.objects.all()
#     serializer_class = SubjectSerializer


@api_view(('GET', ))
# @authentication_classes([LoginRequiredAuthentication, ])
def show_teachers(request):
    """获取老师数据"""
    try:
        sno = int(request.GET['sno'])
        subject = Subject.objects.only('name').get(no=sno)
        queryset = Teacher.objects.defer('subject').filter(subject=sno)
        serializer = TeacherSerializer(queryset, many=True)
        data = {
            'code': 20000,
            'subject': {'no': subject.no, 'name': subject.name},
            'teachers': serializer.data
        }
    except (KeyError, ValueError, Subject.DoesNotExist) as err:
        logging.getLogger('django.request').error(err)
        data = {'code': 20001, 'message': '获取学科老师数据失败'}
    return Response(data)


# class TeacherView(ListAPIView):
#     serializer_class = TeacherSerializer
#     pagination_class = None
#
#     def get_queryset(self):
#         queryset = Teacher.objects.defer('subject')
#         try:
#             sno = self.request.GET.get('sno', '')
#             queryset = queryset.filter(subject__no=sno)
#             return queryset
#         except ValueError:
#             raise Http404('No teachers found.')


def make_comment(request):
    """好评或差评"""
    try:
        user = User.objects.get(no=request.session.get('userid'))
        if user.vote_count > 0:
            tno = int(request.GET['tno'])
            teacher = Teacher.objects.get(no=tno)
            # 通过request.path属性获取请求的资源路径
            if request.path.startswith('/praise'):
                teacher.good_count += 1
                count = teacher.good_count
            else:
                teacher.bad_count += 1
                count = teacher.bad_count
            user.vote_count -= 1
            # 通过atomic函数实现事务操作（事务上下文）
            with atomic():
                teacher.save()
                user.save()
            data = {'code': 10000, 'message': '投票成功', 'count': count}
        else:
            data = {'code': 10003, 'message': '今日票数已全部用完'}
    except (KeyError, ValueError, DatabaseError) as err:
        logging.getLogger('django.request').error(err)
        data = {'code': 10001, 'message': '投票失败'}
    return JsonResponse(data)


@api_view(('POST', ))
def login(request):
    """用户登录"""
    hint = request.GET.get('hint') or ''
    if request.method == 'POST':
        username = request.data.get('username', '').strip()
        password = request.data.get('password', '')
        if check_username(username) and check_password(password):
            password = gen_sha256_digest(password)
            user = User.objects.filter(Q(username=username) | Q(tel=username))\
                .filter(password=password).first()
            if user:
                user.last_visit = timezone.now()
                user.save()
                payload = {
                    'userid': user.no,
                    'exp': timezone.now() + timedelta(days=1)
                }
                # 通过PyJWT的encode函数生成用户身份令牌（bytes，可以通过decode方法处理成str）
                token = jwt.encode(payload, settings.SECRET_KEY).decode()
                return Response({'code': 40000, 'hint': '登录成功', 'token': token, 'username': user.username})
            else:
                hint = '登录失败，用户名或密码错误'
        else:
            hint = '请输入有效的登录信息'
    return Response({'code': 40001, 'hint': hint})


def logout(request):
    """注销（退出登录）"""
    request.session.flush()
    resp = redirect('/')
    resp.delete_cookie('username')
    return resp


def register(request):
    """用户注册"""
    username, tel, hint = '', '', ''
    if request.method == 'POST':
        agreement = request.POST.get('agreement')
        username = request.POST.get('username', '').strip()
        password = request.POST.get('password', '')
        tel = request.POST.get('tel', '').strip()
        if agreement == 'on':
            redis_cli = get_redis_connection()
            code_from_user = request.POST.get('mobilecode', '0')
            code_from_redis = redis_cli.get(f'vote2003:polls:mobile:valid:{tel}').decode()
            if code_from_user == code_from_redis:
                if check_username(username) and check_password(password) and check_tel(tel):
                    password = gen_sha256_digest(password)
                    try:
                        user = User(username=username, password=password, tel=tel)
                        user.last_visit = timezone.now()
                        user.save()
                        # 验证码只能消费一次，注册成功用过的验证码立即失效
                        redis_cli.delete(f'vote2003:polls:mobile:valid:{tel}')
                        hint = '注册成功，请登录'
                        return redirect(f'/login/?hint={hint}')
                    except DatabaseError:
                        hint = '注册失败，用户名或手机号已被使用'
                else:
                    hint = '请输入有效的注册信息'
            else:
                hint = '请输入正确的手机验证码'
        else:
            hint = '请勾选同意网站用户协议及隐私政策'
    return render(request, 'register.html', {'hint': hint, 'username': username, 'tel': tel})


def check_unique(request):
    flag = False
    username = request.GET.get('username', '').strip()
    if check_username(username):
        user = User.objects.filter(username=username).first()
        flag = user is None
    return JsonResponse({'is_valid': flag})


def report(request):
    return render(request, 'report.html')


def export_excel(request):
    wb = xlwt.Workbook()
    sheet = wb.add_sheet('得票统计表')
    titles = ('编号', '姓名', '学科', '好评数', '差评数')
    for col, title in enumerate(titles):
        sheet.write(0, col, title)
    teachers = Teacher.objects.all()\
        .select_related('subject').defer('sex', 'birth', 'intro')
    props = ('no', 'name', 'subject', 'good_count', 'bad_count')
    for row, teacher in enumerate(teachers):
        for col, prop in enumerate(props):
            value = str(getattr(teacher, prop, ''))
            sheet.write(row + 1, col, value)
    buffer = BytesIO()
    wb.save(buffer)
    resp = HttpResponse(buffer.getvalue(), content_type='application/vnd.ms-excel')
    # 设置浏览器Cookie中保存的信息
    # resp.set_cookie('username', 'luohao', max_age=86400)
    # 删除浏览器中的指定的Cookie数据
    # resp.delete_cookie('username')
    # 设置HTTP响应头
    # resp['content-type'] = 'application/vnd.ms-excel'
    # 通过content-disposition响应头告诉浏览器如何处置服务器返回的内容
    # inline - 内联打开；attachment - 下载附件
    filename = quote('老师.xls', encoding='utf-8')
    resp['content-disposition'] = f'attachment; filename*=utf-8\'\'{filename}'
    return resp


def get_bar_data(request):
    teachers = Teacher.objects.all()\
            .only('name', 'good_count', 'bad_count')\
            .order_by('-good_count')[:5]
    x_data, y1_data, y2_data = [], [], []
    for teacher in teachers:
        x_data.append(teacher.name)
        y1_data.append(teacher.good_count)
        y2_data.append(teacher.bad_count)
    return JsonResponse({'x': x_data, 'y1': y1_data, 'y2': y2_data})


def get_mobilecode(request, tel):
    """获取短信验证码"""
    if check_tel(tel):
        redis_cli = get_redis_connection()
        if redis_cli.exists(f'vote2003:polls:mobile:block:{tel}'):
            data = {'code': 30002, 'message': '请不要在60秒内重复发送短信验证码'}
        else:
            code = random_code()
            # 启动一个线程来执行耗时间的任务
            # Thread(target=send_mobile_code, args=(tel, code)).start()
            # future = POOL.submit(send_mobile_code, tel, code)
            # 可以通过submit方法返回的Future对象添加线程执行结束后的回调函数
            # future.add_done_callback(fn)
            # 阻止用户60秒内重复发送短信验证码
            # 异步消息的生产者：将发短信的任务放到消息队列中交给消费者去处理
            # send_mobile_code.delay(tel, code)
            # 启动消息的消费者：celery -A 模块名 worker -Q Q1 -l info
            # Windows环境兼容问题：
            # pip install eventlet
            # celery -A 模块名 worker -Q Q1 -l info -P solo (Python 3.7+)
            # celery -A 模块名 worker -Q Q1 -l info -P eventlet (Python 3.6-)
            # pip install eventlet==0.26.0
            # celery -A 模块名 worker -Q Q1 -l info -P eventlet
            send_mobile_code.apply_async((tel, code), queue='Q1')
            redis_cli.set(f'vote2003:polls:mobile:block:{tel}', 'x', ex=60)
            # 将验证码在Redis中保留10分钟（有效期）
            redis_cli.set(f'vote2003:polls:mobile:valid:{tel}', code, ex=600)
            data = {'code': 30000, 'message': '短信验证码已发送，请注意查收'}
    else:
        data = {'code': 30001, 'message': '请输入有效的手机号'}
    return JsonResponse(data)


@csrf_exempt
def upload(request):
    # request.GET ---> 获取URL问号后面的查询参数
    # request.POST ---> 获取表单数据
    # request.FILES ---> 获取通过表单上传的文件
    # request.META ---> 获取请求相关元信息（请求头）
    # request.COOKIES ---> 获取请求中携带的Cookie信息
    # request.data ---> 获取异步请求携带的数据（通常指JSON格式的数据）
    # type(photo) ---> InMemoryUploadedFile（小于2.5M） / TemporaryUploadedFile（大于2.5M）
    photo = request.FILES.get('photo')
    # photo.name ---> 获取上传文件的文件名
    # photo.size ---> 获取上传文件的大小
    # 通过os.path.splitext将文件名拆分为文件名和扩展名两部分
    _, ext = os.path.splitext(photo.name)
    # 通过UUID和原来文件的扩展名生成新的文件名
    filename = f'{uuid.uuid1().hex}{ext}'
    upload_stream_to_qiniu(filename, photo.file, photo.size)
    return redirect('/static/html/upload.html')
